Skip to content

feat(connector)!: expose SUPPORT_DYN_VC_GFX_PROTOCOL early-cap flag for EGFX clients - #1237

Open
GlassOnTin wants to merge 1 commit into
Devolutions:masterfrom
GlassOnTin:feat/egfx-early-cap-flag
Open

feat(connector)!: expose SUPPORT_DYN_VC_GFX_PROTOCOL early-cap flag for EGFX clients#1237
GlassOnTin wants to merge 1 commit into
Devolutions:masterfrom
GlassOnTin:feat/egfx-early-cap-flag

Conversation

@GlassOnTin

Copy link
Copy Markdown
Contributor

Currently, early_capability_flags in the GCC core data is built from a fixed set in connection.rs. Clients that want to use the Graphics Pipeline Extension (MS-RDPEGFX) — by attaching a DvcClientProcessor for Microsoft::Windows::RDS::Graphics — have no way to set SUPPORT_DYN_VC_GFX_PROTOCOL without forking the connector, and modern Windows servers won't open the EGFX channel unless the client advertises support.

This PR adds an opt-in Config.support_dyn_vc_gfx_protocol: bool (default false). When set, the flag is OR'd into early_capability_flags alongside the existing WANT_32_BPP_SESSION conditional. Existing consumers are unaffected; the doc comment includes a safety note that setting this without an EGFX implementation will cause Windows to stop sending legacy bitmap updates, leaving the desktop blank.

Used downstream by Haven (an Android RDP/VNC client) which implements EGFX with ClearCodec + RemoteFxProgressive decoders; this lets us drop a vendored fork of ironrdp-connector we currently carry just for this one flag.

Default false to preserve current behaviour. cargo check --workspace clean — six other in-tree Config { … } builders updated with the default-false field.

@glamberson

Copy link
Copy Markdown
Contributor

Reading through, this is the smallest possible fix and the doc-comment captures the foot-gun precisely. Without wiring a DvcClientProcessor for the Graphics channel, modern Windows servers stop sending legacy bitmap updates entirely and the desktop goes blank, which is exactly the GRD case in #1232 and the failure mode every downstream EGFX client hits.

No conflict with #1098 (multitransport bootstrapping) on my read of the GCC block area, just two independent flags being OR'd in. Looks good from the server-side perspective too: any IronRDP server that already speaks EGFX (lamco-rdp-server, GRD) sees this flag and routes graphics through DVC as expected.

@glamberson

Copy link
Copy Markdown
Contributor

GlassOnTin Hi, could you rebase this? I'm trying to get this prioritized, and this would be a first step on the client side work I'm looking t. Thanks!

@GlassOnTin
GlassOnTin force-pushed the feat/egfx-early-cap-flag branch from 84ddffa to dbe5d82 Compare July 20, 2026 19:24
@GlassOnTin

Copy link
Copy Markdown
Contributor Author

Done — rebased onto current master and force-pushed. It's a single commit again and GitHub now shows it as mergeable.

The only conflicts were in the test builders, where master had restructured things since I opened this: session/autodetect.rs's local test_config() and the monolithic testsuite-extra/tests/main.rs were removed/split, so I dropped my now-obsolete edits to those and instead added support_dyn_vc_gfx_protocol: false to the Config { … } literals in their new homes — connector/autodetect.rs, session/connection_activation.rs, and e2e.rs. The core change (the early_capability_flags OR in connection.rs and the field on Config) merged cleanly.

cargo check --workspace --all-targets is clean. Happy to squash/adjust further if you'd like it shaped differently for the client-side work.

@glamberson

Copy link
Copy Markdown
Contributor

Thank you! Benoît Cortier (@CBenoit) can you expedite this please? Thank you. My client issue will be posted later today.

@GlassOnTin

Copy link
Copy Markdown
Contributor Author

For reviewer context: I've opened #1472 as a sibling to this — a small ironrdp-pdu fix to tolerate an empty Server Font Map body (another VirtualBox VRDP quirk). Together these two are the last things a downstream Android RDP client needs to drop a vendored ironrdp-connector fork it currently carries: this PR removes the EGFX early-capability workaround, #1472 removes the Font Map one. Both are opt-in / strictly-more-lenient and don't change existing behaviour. No rush — just flagging they land the same cleanup.

Marc-André Moreau (mamoreau-devolutions) pushed a commit that referenced this pull request Jul 31, 2026
…tionResult (#1488)

Per [MS-RDPBCGR] 2.2.8.1.2, a client must not send fast-path input
events unless the server advertised `INPUT_FLAG_FASTPATH_INPUT` or
`INPUT_FLAG_FASTPATH_INPUT2` in its Input Capability Set. Today the
connector discards the server's capability sets after Demand Active, so
client code has no way to honour that requirement.

This PR captures the server's Input capability flags during capabilities
exchange, carries them through the `ConnectionFinalization`/`Finalized`
activation states (so they are refreshed correctly across a
Deactivation-Reactivation Sequence too), and surfaces them as
`ConnectionResult::input_flags`. The session layer can then choose
between fast-path and slow-path input per server.

### Motivation / real-world interop

This is not theoretical: VirtualBox's VRDP server closes the connection
outright on receiving a fast-path input PDU — its `VBox.log` reports

```
VRDP: Network packet length is incorrect 0x0004. Closing connection.
```

(a single fast-path scancode event is a 4-byte packet). VirtualBox never
advertises fast-path input; its Demand Active offers
`InputFlags(SCANCODES)` alone. mstsc and FreeRDP honour the negotiation
and fall back to slow-path `TS_INPUT_PDU`s, which is why they work
against VRDE.

Haven (Android RDP client built on IronRDP) has been shipping this exact
change as a vendored-connector patch since v5.86.1, with the slow-path
fallback keyed off `ConnectionResult::input_flags`. Verified against a
real VirtualBox 7.2.6 VRDE server: before the gate, the first arrow-key
press killed the session with the log line above; with the gate,
extended input sessions run clean, and a fast-path-capable server on the
same host still takes the fast-path branch. (Discussed in #1158; this is
the third and last piece Haven carries in its connector fork, alongside
#1237 and #1472.)

### Changes

- `connection_activation.rs`: capture `input_flags` from the
`CapabilitySet::Input` in Server Demand Active (empty if absent); add
the field to `ConnectionActivationState::{ConnectionFinalization,
Finalized}`.
- `connection.rs`: add `ConnectionResult::input_flags`, populated from
the `Finalized` state.
- Call sites in `ironrdp-client`, `ironrdp-web`, ffi, and the e2e test
updated for the new variant field (all currently ignore it).

### Testing

- Two new integration tests in
`ironrdp-testsuite-core/tests/session/connection_activation.rs`: the
fixture's Demand Active yields `SCANCODES | MOUSEX | UNICODE |
FASTPATH_INPUT_2` in the `ConnectionFinalization` state, and a Demand
Active with the Input capability stripped yields `InputFlags::empty()`.
- `cargo check --workspace --all-targets`, `cargo clippy --workspace
--all-targets`, and `cargo fmt --check` are clean; the 7 activation
tests pass.

Co-authored-by: GlassOnTin <GlassOnTin@users.noreply.github.com>
@github-actions github-actions Bot added A-web-client Area: Web client (ironrdp-web, iron-remote-gui, iron-svelte-client) A-core Area: Core tier A-extra Area: Extra tier A-internal Area: Internal tier A-server Area: RDP server rust Pull requests that update Rust code size/S Size: 30-150 lines of code labels Jul 31, 2026
@CBenoit

Copy link
Copy Markdown
Member

Adding support_dyn_vc_gfx_protocol to the exhaustively constructible public Config struct is source-incompatible for downstream struct literals. The EGFX capability bit is correctly opt-in and preserves the false default; enabled-wire-bit and end-to-end negotiation coverage remain valuable.

…or EGFX clients

Adds an opt-in `Config::support_dyn_vc_gfx_protocol` (default `false`). When
set, `SUPPORT_DYN_VC_GFX_PROTOCOL` is OR'd into the client's early capability
flags, so the server may negotiate `Microsoft::Windows::RDS::Graphics` for
surface-based graphics instead of the legacy slow-path bitmap protocol.

BREAKING CHANGE: `Config` is not `#[non_exhaustive]`, so the new field breaks
downstream struct literals. Same shape as Devolutions#1501, which added
`auto_reconnect_cookie` to this struct.

The default is `false` on purpose. A client that advertises the bit without
wiring an EGFX-capable `DvcClientProcessor` for that channel gets modern
Windows servers to stop sending legacy bitmap updates altogether and route
everything over EGFX, leaving the desktop blank — so the doc comment spells
out the three steps to enable it properly.

Coverage:
  - create_gcc_blocks unit tests (inline mod at file end, per Devolutions#1527) assert
    the bit is clear by default, set when enabled, and that the opt-in's
    symmetric difference against the default flag set is exactly that one bit
    — so it cannot quietly disturb the others. Verified to fail: gating on
    `if false` reddens two of the three.
  - testsuite-extra e2e runs a full client<->server connect with the bit
    advertised. Scope stated in the test: it asserts negotiation completes,
    not that the server acted on it, since ironrdp-server does not surface
    the client's early capability flags.
@GlassOnTin

Copy link
Copy Markdown
Contributor Author

Thanks — both points taken. Rebased onto current master and addressed them.

On the source incompatibility: you're right, and I've stopped pretending otherwise — the PR is now marked breaking (feat(connector)!:). Config is not #[non_exhaustive], so any added field breaks downstream struct literals, and this one is no exception.

For what it's worth, #1501 added pub auto_reconnect_cookie: Option<ServerAutoReconnect> to this same struct and shipped as feat(pdu,session,connector,server)!: — so if that's the accepted shape for this kind of change, this now matches it. If you'd rather absorb the break once and be done with it, making Config #[non_exhaustive] with a Default impl or a builder would mean this field and every future one costs nothing downstream; I'm happy to do that as a separate PR ahead of this one, but it's a bigger call than mine to make.

On the coverage: added both.

Wire bit, in ironrdp-connector's create_gcc_blocks (inline mod tests at the file end, per #1527) — three tests:

  • dyn_vc_gfx_protocol_bit_is_absent_by_default — the default leaves the bit clear. This is the one that matters most: a client that sets the flag without wiring an EGFX-capable DVC processor gets modern Windows servers to stop sending legacy bitmap updates entirely, and the desktop goes blank.
  • dyn_vc_gfx_protocol_bit_is_set_when_enabled — opting in sets it.
  • dyn_vc_gfx_protocol_opt_in_changes_exactly_one_bit — asserts the symmetric difference between the enabled and disabled flag sets is exactly SUPPORT_DYN_VC_GFX_PROTOCOL, so the opt-in can't quietly disturb anything else.

These are checked to actually fail: replacing the if config.support_dyn_vc_gfx_protocol gate with if false reddens the second and third and correctly leaves the first green.

End to end, in testsuite-extra: test_client_server_advertising_dyn_vc_gfx_protocol runs the full client↔server connect with the bit advertised.

I want to be straight about that last one's scope rather than oversell it: it asserts negotiation completes with the bit set, not that the server acted on it. ironrdp-server doesn't surface the client's early capability flags, and plumbing that through purely to observe it in a test would have been a larger change than the feature itself. The real assertion lives in the unit tests above. If you'd like the server side to expose received early caps — useful beyond this PR — say so and I'll open it separately.

cargo fmt --check, cargo check --workspace --all-targets and cargo clippy -p ironrdp-connector --all-targets -- -D warnings are clean.

@GlassOnTin
GlassOnTin force-pushed the feat/egfx-early-cap-flag branch from dbe5d82 to 2ee58fb Compare August 3, 2026 22:25
@GlassOnTin GlassOnTin changed the title feat(connector): expose SUPPORT_DYN_VC_GFX_PROTOCOL early-cap flag for EGFX clients feat(connector)!: expose SUPPORT_DYN_VC_GFX_PROTOCOL early-cap flag for EGFX clients Aug 3, 2026
GlassOnTin added a commit to GlassHaven/Haven that referenced this pull request Aug 3, 2026
The fork carried three patches. Two are now upstream — the server Input
capability flags (#1488) and the lenient Server Font Map (#1506, which
superseded our #1472 with a better-scoped fix: ours made FontPdu::decode
lenient generally, and FontPdu is shared with Font List, so a malformed
header-only *client* Font List would have decoded and advanced acceptor
finalization). The third, the SUPPORT_DYN_VC_GFX_PROTOCOL early-capability
flag, is still open as Devolutions/IronRDP#1237 and is the only thing our
fork now adds on top of upstream master.

What the released crates cost us: ironrdp-session 0.11.0 indexes the
framebuffer from the decompressed buffer's length while bounds-checking
only the destination rectangle, so a bitmap taller than its own dest rect
writes past the end and panics. That is the #422 crash that closes Haven
outright. Upstream fixed it in #1521 (2026-08-02) and no release carries
it, so waiting for one means knowingly shipping an app-killing crash.

Pinned to an immutable tag, haven-pin-20260803 = 2ee58fb1 = upstream
e9e0bb49 + the #1237 patch. A tag rather than a bare rev because
feat/egfx-early-cap-flag is an open PR that gets force-pushed on rebase,
and the pinned object has to stay reachable. All thirteen ironrdp crates
go through one [patch.crates-io] block: mixing git and crates.io links two
copies of ironrdp-pdu and the types stop matching.

Verified present in the pinned tree rather than assumed:
  - #1237 flag        connector/src/connection.rs:1370
  - #1488 input_flags on ConnectionResult
  - #1506 Font Map    the FontMap dispatch arm, not FontPdu::decode
  - #1521 require_bitmap_data_size on all 7 apply_*_bitmap paths,
    including the bgr24 one the #422 tombstone points at

COST: this forces the Rust toolchain 1.89.0 -> 1.94.1. Upstream raised its
workspace rust-version in #1495 and #1521 landed after it, so there is no
commit carrying the fix and the old floor; cargo refuses outright.
rust-toolchain.toml and CI's `rustup default` move together. spice-kotlin
keeps its own 1.89.0 pin — nothing there needs 1.94, and a
directory-scoped toml lets them differ.

Migration for four upstream breaking changes:
  - Config gains connection_type and support_dyn_vc_gfx_protocol (set true
    — Haven has the EGFX DVC processor wired, which is the foot-gun the
    upstream flag documents)
  - ConnectionActivationState::Finalized gains refresh_rect_support and
    suppress_output_support; reactivation reuses connect-time decisions,
    so the pattern takes `..`
  - ProcessorBuilder loses bulk_decompressor (#1255) — the processor always
    owns one now, so a reactivation no longer drops the decompression
    history a later compressed update refers back to
  - ActiveStageOutput gains SaveSessionInfo and AutoReconnectCookie
    (#1501). Both logged, not acted on: there is no session-resume path to
    spend the cookie on, and retaining a credential-equivalent token we
    would never use is not a trade worth making.

80/80 rust tests pass; all 3 ABIs rebuilt (the pin is inert until they
are). arm64 librdp_transport.so 6,411,856 -> 6,185,896 bytes: the newer
compiler and deps give back ~226 KB of what panic="unwind" cost.

NOT verified: F-Droid is the authoritative build environment and a cargo
*git* dependency has never been exercised there. Their recipe fetches from
crates.io today and does `rustup default 1.85.0` (the crate-scoped toml
overrides it, as it already does for 1.89). If the next F-Droid build
fails on either, that is where to look first.

panic="unwind" stays despite #1521. The tombstone frames were never
symbolized, so attributing that crash to apply_bgr24_bitmap is inference;
dropping the safety net to reclaim the size would be acting on it as if it
were proven.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-core Area: Core tier A-extra Area: Extra tier A-internal Area: Internal tier A-server Area: RDP server A-web-client Area: Web client (ironrdp-web, iron-remote-gui, iron-svelte-client) ai-reviewed/1 breaking-change human-required risk:medium rust Pull requests that update Rust code size/S Size: 30-150 lines of code

Development

Successfully merging this pull request may close these issues.

3 participants